﻿using System;
using System.CodeDom.Compiler;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Windows.Forms;
using FastColoredTextBoxNS;
using CodeEditorCompiler.Properties;
using System.Text;

namespace CodeEditorCompiler
{
    public partial class TextEditorCompiler : UserControl
    {
        protected MarkerStyle sameWordsStyle = new MarkerStyle( new SolidBrush( Color.FromArgb( 40, Color.Gray ) ) );

        protected int BuildHashCode { get; set; }

        private bool redirectStandardOutput = true;
        public bool RedirectStandardOutput
        {
            get { return redirectStandardOutput; }

            set
            {
                this.redirectStandardOutput = value;
                this.btnRedirect.Image = this.RedirectStandardOutput ? Resources.Redirect : Resources.RedirectDisabled;

                this.btnRedirect.ToolTipText = this.btnRedirect.ToolTipText.Replace( " (enabled)", String.Empty ).Replace( " (disabled)", String.Empty );
                this.btnRedirect.ToolTipText += this.RedirectStandardOutput ? " (enabled)" : " (disabled)";
            }
        }

        private bool showOutputWindow = false;
        public bool ShowOutputWindow
        {
            get
            {
                return this.showOutputWindow;
            }

            set
            {
                this.showOutputWindow = value;
                this.btnOutputView.Image = this.ShowOutputWindow ? Resources.SingleView : Resources.DoubleView;

                this.btnOutputView.ToolTipText = this.btnOutputView.ToolTipText.Replace( " (shown)", String.Empty ).Replace( " (hidden)", String.Empty );
                this.btnOutputView.ToolTipText += this.ShowOutputWindow ? " (shown)" : " (hidden)";

                this.splitContainer1.Panel2Collapsed = !this.ShowOutputWindow;
            }
        }

        public event EventHandler BeforeCompile;
        public event EventHandler AfterCompile;

        public event EventHandler BeforeExecute;
        public event EventHandler AfterExecute;

        public TextEditorCompiler()
        {
            InitializeComponent();

            this.cbLanguage.SelectedIndex = 0;
            this.InputTextBox.KeyUp += ( o, e ) =>
            {
                switch( e.KeyCode )
                {
                    case Keys.F5: this.BuildCode(); break;
                    case Keys.F6: this.ExecuteCode(); break;
                }
            };
        }

        /// <summary>
        /// Try to build compilable code.
        /// </summary>
        /// <returns>Null if build did not take place, compiler results otherwise.</returns>
        private CompilerResults Compile()
        {
            if( this.textBoxInput.Language == Language.CSharp || this.textBoxInput.Language == Language.VB )
            {
                this.BuildHashCode = this.InputTextBox.Text.GetHashCode();
                string fileName = String.Format( @"{0}\{1}.dll", Application.StartupPath, this.BuildHashCode.ToString() );

                if( !File.Exists( fileName ) )
                {
                    string languageLiteral = ( this.textBoxInput.Language == Language.CSharp ) ? "C#" : "VB";

                    CodeDomProvider compiler = CodeDomProvider.CreateProvider( languageLiteral );
                    var compilerParameters = new CompilerParameters()
                    {
                        OutputAssembly = fileName,
                        CompilerOptions = "/target:library /optimize",
                        WarningLevel = 3,
                        GenerateExecutable = false,
                        GenerateInMemory = false
                    };

                    compilerParameters.ReferencedAssemblies.Add( "System.dll" );
                    compilerParameters.ReferencedAssemblies.Add( "System.Data.dll" );
                    compilerParameters.ReferencedAssemblies.Add( "System.Xml.dll" );
                    compilerParameters.ReferencedAssemblies.Add( "mscorlib.dll" );
                    compilerParameters.ReferencedAssemblies.Add( "System.Windows.Forms.dll" );
                    compilerParameters.ReferencedAssemblies.Add( "CodeEditorCompiler.dll" );

                    return compiler.CompileAssemblyFromSource( compilerParameters, this.InputTextBox.Text );
                }

                return null;
            }

            throw new UncompilableException( "You can only compile C# or VB.NET code" );
        }

        private void Execute( string typeNameToInstantiate )
        {
            if( this.BuildHashCode != this.InputTextBox.Text.GetHashCode() )
            {
                DialogResult dlgRes = MessageBox.Show( "You need to compile before execute this code. Would you like to build now?", "Re-build needed", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question );

                switch( dlgRes )
                {
                    case DialogResult.Yes: this.Compile(); break;
                    case DialogResult.Cancel: return;
                }
            }

            AppDomain newDomain = AppDomain.CreateDomain( String.Format( "Domain{0}", this.BuildHashCode ) );

            byte[] rawAssembly = File.ReadAllBytes( this.BuildHashCode + ".dll" );
            Assembly assembly = newDomain.Load( rawAssembly, null );

            IExecutableScript testClass = (IExecutableScript)assembly.CreateInstance( typeNameToInstantiate );
            testClass.Execute();

            testClass = null;
            assembly = null;

            AppDomain.Unload( newDomain );
            newDomain = null;
        }

        private void btnExecute_Click( object sender, EventArgs e )
        {
            this.ExecuteCode();
        }

        public void ExecuteCode()
        {
            if( BeforeExecute != null )
                BeforeExecute( this, null );

            if( this.RedirectStandardOutput )
            {
                this.splitContainer1.Panel2Collapsed = false;
                this.OutputTextBox.Clear();
                this.OutputTextBox.ForeColor = Color.Blue;
                FeedbackWriter writer = new FeedbackWriter( this.OutputTextBox );
                Console.SetOut( writer );
            }
            else
            {
                Console.SetOut( new StreamWriter( Console.OpenStandardOutput() ) );
            }

            this.Execute( "Script.Test" );

            if( AfterExecute != null )
                AfterExecute( this, null );
        }

        private void btnBuild_Click( object sender, EventArgs e )
        {
            this.BuildCode();
        }

        public void BuildCode()
        {
            if( BeforeCompile != null )
                BeforeCompile( this, null );

            this.splitContainer1.Panel2Collapsed = false;
            this.OutputTextBox.Clear();
            this.OutputTextBox.AppendText( Color.Green, "Build started..." + Environment.NewLine );
            var compilerResults = this.Compile();

            if( compilerResults != null && compilerResults.Errors.Count > 0 )
            {
                this.OutputTextBox.AppendText( Color.Red, "Build failed" + Environment.NewLine );

                foreach( CompilerError ce in compilerResults.Errors )
                    this.OutputTextBox.AppendText( Color.Red, ce.ErrorNumber + ": " + ce.ErrorText + Environment.NewLine );
            }
            else if( compilerResults != null && compilerResults.Errors.Count == 0 )
            {
                this.OutputTextBox.AppendText( Color.Green, "Build succeeded" + Environment.NewLine );
            }
            else if( compilerResults == null )
            {
                this.OutputTextBox.AppendText( Color.Green, "Build already up-to-date" + Environment.NewLine );
            }

            if( AfterCompile != null )
                AfterCompile( this, null );
        }

        private void textBoxInput_TextChanged( object sender, TextChangedEventArgs e )
        {
            this.splitContainer1.Panel2Collapsed = true;
        }

        private void btnRedirect_Click( object sender, EventArgs e )
        {
            this.RedirectStandardOutput = !this.RedirectStandardOutput;
        }

        public void LoadCSharpSkeleton()
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendLine( "//To execute and test you code, please implement IExecutableScript interface." );
            sb.AppendLine( "//Please note this component is mainly for compiling and returning a reference to your new assembly." );
            sb.AppendLine( "//Execute feature is provided mainly for testing purposes." );
            sb.AppendLine( "//You may need to write extra code to instantiate classes and/or run methods in more advanced scenarios." );

            sb.AppendLine();
            sb.AppendLine( "using System;" );
            sb.AppendLine( "using System.Collections;" );
            sb.AppendLine( "using System.IO;" );
            sb.AppendLine( "using System.Windows.Forms;" );
            sb.AppendLine( "using CodeEditorCompiler;" );
            sb.AppendLine();
            sb.AppendLine( "namespace Script" );
            sb.AppendLine( "{" );
            sb.AppendLine( "	public class Test : IExecutableScript" );
            sb.AppendLine( "	{" );
            sb.AppendLine( "		public Test()" );
            sb.AppendLine( "		{" );
            sb.AppendLine( "		}" );
            sb.AppendLine();
            sb.AppendLine( "		public void Execute() //IExecutableScript interface method" );
            sb.AppendLine( "		{" );
            sb.AppendLine( "			Console.WriteLine(\"This is a test message\");" );
            sb.AppendLine( "			MessageBox.Show(\"Executed\");" );
            sb.AppendLine( "		}" );
            sb.AppendLine( "	}" );
            sb.AppendLine( "}" );

            this.InputTextBox.Text = sb.ToString();
        }

        private void btnSkeletonCode_Click( object sender, EventArgs e )
        {
            if( this.cbLanguage.Text == "C#" )
                this.LoadCSharpSkeleton();
            else if( this.cbLanguage.Text == "VB" )
                this.LoadVbSkeleton();
        }

        private void LoadVbSkeleton()
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendLine( "'To execute and test your code, please implement IExecutableScript interface." );
            sb.AppendLine( "'Please note this component is mainly for compiling and returning a reference to your new assembly." );
            sb.AppendLine( "'Execute feature is provided mainly for testing purposes." );
            sb.AppendLine( "'You may need to write extra code to instantiate classes and/or run methods in more advanced scenarios." );

            sb.AppendLine();
            sb.AppendLine( "Imports System" );
            sb.AppendLine( "Imports System.Collections" );
            sb.AppendLine( "Imports System.IO" );
            sb.AppendLine( "Imports System.Windows.Forms" );
            sb.AppendLine( "Imports CodeEditorCompiler" );
            sb.AppendLine();
            sb.AppendLine( "Namespace Script" );
            sb.AppendLine();
            sb.AppendLine( "    Public Class Test Implements IExecutableScript" );
            sb.AppendLine();
            sb.AppendLine( "        Public Sub New() MyBase.New" );
            sb.AppendLine();
            sb.AppendLine( "        End Sub" );
            sb.AppendLine();
            sb.AppendLine( "        Public Sub Execute()  'IExecutableScript interface method" );
            sb.AppendLine( "            Console.WriteLine(\"This is a test message\")" );
            sb.AppendLine( "            MessageBox.Show(\"Executed\")" );
            sb.AppendLine( "        End Sub" );
            sb.AppendLine();
            sb.AppendLine( "    End Class" );
            sb.AppendLine();
            sb.AppendLine( "End Namespace" );

            this.InputTextBox.Text = sb.ToString();
        }

        private void cbLanguage_SelectedIndexChanged( object sender, EventArgs e )
        {
            this.InputTextBox.ClearStylesBuffer();
            this.InputTextBox.Range.ClearStyle( StyleIndex.All );

            if( this.cbLanguage.Text == "C#" )
                this.InputTextBox.Language = Language.CSharp;
            else if( this.cbLanguage.Text == "VB" )
                this.InputTextBox.Language = Language.VB;

            this.InputTextBox.OnTextChanged();
        }

        private void textBoxInput_SelectionChangedDelayed( object sender, EventArgs e )
        {
            this.InputTextBox.VisibleRange.ClearStyle( sameWordsStyle );

            if( !this.InputTextBox.Selection.IsEmpty )
                return;//user selected diapason

            //get fragment around caret
            var fragment = this.InputTextBox.Selection.GetFragment( @"\w" );
            string text = fragment.Text;
            if( text.Length == 0 )
                return;

            //highlight same words
            var ranges = this.InputTextBox.VisibleRange.GetRanges( @"\b" + text + @"\b" );
            foreach( var range in ranges )
                range.SetStyle( sameWordsStyle );
        }

        private void btnOutputView_Click( object sender, EventArgs e )
        {
            this.ShowOutputWindow = !this.ShowOutputWindow;
        }
    }
}
